using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;

namespace Comms {
    public class NetworkSimulatorTransmitter : IWrapperTransmitter {
        public volatile bool IsDisposed;

        public int SimulatedMaxPacketSize = int.MaxValue;

        public SortedDictionary<double, List<Action>> PendingActions = new();

        public Task Task;

        public object Lock = new();

        public int RndSeed;

        public ITransmitter BaseTransmitter { get; set; }

        public NetworkSimulatorStats Stats { get; }

        public float MinimumDelay { get; set; }

        public float MaximumDelay { get; set; }

        public float DropRatio { get; set; }

        public float DuplicateRatio { get; set; }

        public float ByteCorruptRatio { get; set; }

        public float TruncateRatio { get; set; }

        public int MaxPacketSize {
            get => Math.Max(Math.Min(SimulatedMaxPacketSize, BaseTransmitter.MaxPacketSize), 0);
            set => SimulatedMaxPacketSize = value;
        }

        public IPEndPoint Address => BaseTransmitter.Address;

        public event Action<Exception> Error;

        public event Action<string> Debug {
            add => BaseTransmitter.Debug += value;
            remove => BaseTransmitter.Debug -= value;
        }

        public event Action<Packet> PacketReceived;

        public NetworkSimulatorTransmitter(ITransmitter baseTransmitter) : this(baseTransmitter, new NetworkSimulatorStats()) { }

        public NetworkSimulatorTransmitter(ITransmitter baseTransmitter, NetworkSimulatorStats stats) {
            RndSeed = GetHashCode();
            Stats = stats;
            BaseTransmitter = baseTransmitter ?? throw new ArgumentNullException("baseTransmitter");
            BaseTransmitter.Error += delegate(Exception e) { Error?.Invoke(e); };
            BaseTransmitter.PacketReceived += delegate(Packet packet) {
                if (Stats != null) {
                    Stats.LastActivityTicks = Environment.TickCount & 0x7FFFFFFF;
                }
                if (DropRatio <= 0f
                    || !RndBool(DropRatio)) {
                    if (Stats != null) {
                        Interlocked.Increment(ref Stats.PacketsReceived);
                        Interlocked.Add(ref Stats.BytesReceived, packet.Bytes.Length);
                    }
                    PacketReceived?.Invoke(packet);
                }
            };
            Task = new Task(
                delegate {
                    Thread.CurrentThread.Name = "NetworkSimulatorTransmitter";
                    while (!IsDisposed) {
                        try {
                            ExecutePendingActions();
                            Thread.Sleep(10);
                        }
                        catch (Exception obj) {
                            Error?.Invoke(obj);
                        }
                    }
                },
                TaskCreationOptions.LongRunning
            );
            Task.Start();
        }

        public void Dispose() {
            if (!IsDisposed) {
                IsDisposed = true;
                Task.Wait();
                BaseTransmitter.Dispose();
            }
        }

        public void SendPacket(Packet packet) {
            lock (Lock) {
                if (Stats != null) {
                    Stats.LastActivityTicks = Environment.TickCount & 0x7FFFFFFF;
                    Interlocked.Increment(ref Stats.PacketsSent);
                    Interlocked.Add(ref Stats.BytesSent, packet.Bytes.Length);
                }
                if (DropRatio <= 0f
                    || !RndBool(DropRatio)) {
                    if (TruncateRatio > 0f
                        && packet.Bytes.Length != 0
                        && RndBool(TruncateRatio)) {
                        packet.Bytes = packet.Bytes.Take(RndInt(packet.Bytes.Length)).ToArray();
                    }
                    if (ByteCorruptRatio > 0f) {
                        packet.Bytes = packet.Bytes.ToArray();
                        for (int i = 0; i < packet.Bytes.Length; i++) {
                            if (RndBool(ByteCorruptRatio)) {
                                packet.Bytes[i] = (byte)RndInt();
                            }
                        }
                    }
                    if (DuplicateRatio > 0f
                        && RndBool(DuplicateRatio)) {
                        QueueAction(RandomizeDelay(), delegate { SendPacket(packet); });
                    }
                    if (MinimumDelay > 0f
                        || MaximumDelay > 0f) {
                        QueueAction(RandomizeDelay(), delegate { BaseTransmitter.SendPacket(packet); });
                    }
                    else {
                        BaseTransmitter.SendPacket(packet);
                    }
                }
                else if (Stats != null) {
                    Interlocked.Increment(ref Stats.PacketsDropped);
                }
            }
        }

        public void QueueAction(double delay, Action action) {
            double time = Comm.GetTime();
            double num = Math.Round((time + delay) * 100.0) / 100.0;
            if (num <= time) {
                action();
                return;
            }
            lock (Lock) {
                if (!PendingActions.TryGetValue(num, out List<Action> value)) {
                    value = new List<Action>();
                    PendingActions.Add(num, value);
                }
                value.Add(action);
            }
        }

        public void ExecutePendingActions() {
            lock (Lock) {
                double time = Comm.GetTime();
                while (PendingActions.Count > 0) {
                    KeyValuePair<double, List<Action>> keyValuePair = PendingActions.First();
                    if (keyValuePair.Key > time) {
                        break;
                    }
                    PendingActions.Remove(keyValuePair.Key);
                    foreach (Action item in keyValuePair.Value) {
                        item();
                    }
                }
            }
        }

        public double RandomizeDelay() {
            float minimumDelay = MinimumDelay;
            float num = Math.Max(MaximumDelay, MinimumDelay);
            return RndInt() / 2147483648.0 * (num - minimumDelay) + minimumDelay;
        }

        public int RndInt() {
            Interlocked.Increment(ref RndSeed);
            return (int)(Hash((uint)RndSeed) & 0x7FFFFFFF);
        }

        public int RndInt(int bound) => RndInt() % bound;

        public bool RndBool(double probability) => RndInt() < probability * 2147483648.0;

        public static uint Hash(uint key) {
            key ^= key >> 16;
            key *= 2146121005;
            key ^= key >> 15;
            key *= 2221713035u;
            key ^= key >> 16;
            return key;
        }
    }
}